#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (C) 1994  Ling Li
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.


import string
from threading import Thread, Lock, Event
from Queue import Queue
import pexpect
import os
import thread
from logging import debug, info, error, warning

from transfer_view import *
from log_view import LogViewUpdater
from configuration import config_value
import ftp_engine
from ftp_engine import *
from ftp_exception import *
from utils import *

class LftpClient:

    LFTP_PROGRAM = 'lftp'
    PROMPT_STRING = "__coralftp__"

    def __init__(self, ftp_engine):
        self.__ftp_engine = ftp_engine
        self.__config = self.__ftp_engine.config

        # new debug info listener thread
        self.__fifo_path = os.tmpnam()
        self.__debug_listening = DebugListeningThread(self,
                                                      ftp_engine.log_updater)

    def __del__(self):
        if self.__proc:
            self.__proc.close(0)

    def run(self):
        self.__debug_listening.start()
        logging.debug('lftp monitor thread %d' % thread.get_ident())
        # start lftp program
        self.__proc = pexpect.spawn("lftp")
        if self.__proc.isalive():
            logging.info("lftp started.")
        else:
            logging.error("failed to start lftp.")

        # initial setup
        self.__proc.sendline("set prompt " + LftpClient.PROMPT_STRING)
        self.__proc.expect("\r" + LftpClient.PROMPT_STRING)
        #print self.__proc.before
        LftpCommand(self, "debug -o %s 5" % self.__fifo_path).execute()
        LftpVersionCommand(self).execute()

        # apply more configuration
        # anonymous password
        value = config_value(self.__config, 'general', 'email_address')
        if value != '':
            LftpCommand(self, 'set ftp:anon-pass "%s"' % value).execute()
        # connect timeout
        value = config_value(self.__config, 'general', 'connect_timeout')
        if value > 0:
            LftpCommand(self, 'set net:timeout %d' % value).execute()
        # connect try delay
        value = config_value(self.__config, 'general', 'connect_retry_delay')
        if value > 0:
            LftpCommand(self, 'set net:reconnect-interval-base %d' % value).execute()
            LftpCommand(self, 'set net:reconnect-interval-max %d' % (value+1)).execute()
            LftpCommand(self, 'set net:reconnect-interval-multiplier 1.0').execute()

    def interrupt(self):
        self.__proc.kill(2)

    def quit(self):
        LftpQuitCommand(self).execute()
    
    def new_command(self, cmd_type, *args):
        if cmd_type == ftp_engine.CMD_QUIT:
            return LftpQuitCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_OPEN:
            return LftpOpenCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_CD:
            return LftpCdCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_LIST:
            return LftpListCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_LISTD:
            return LftpListdCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_PWD:
            return LftpPwdCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_PUT:
            return LftpPutCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_GET:
            return LftpGetCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_MKDIR:
            return LftpMkdirCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_DELETE:
            return LftpDeleteCommand(self, *args)
        elif cmd_type == ftp_engine.CMD_MOVE:
            return LftpMoveCommand(self, *args)
        else:
            raise ValueError

    def execute_command(self, command):
        return command.execute()
        
    def __getattr__(self, name):
        if name == 'ftp_thread':
            return self.__ftp_thread
        elif name == 'proc':
            return self.__proc
        elif name == 'debug_fifo_path':
            return self.__fifo_path
        elif name == 'ftp_engine':
            return self.__ftp_engine
        else:
            raise AttributeError

class DebugListeningThread(Thread):

    def __init__(self, lftp, log_updater):
        Thread.__init__(self)
        self.__lftp = lftp
        self.__path = lftp.debug_fifo_path
        self.__log_updater = log_updater
        os.mkfifo(self.__path)
        return

    def run(self):
        logging.debug("Logging thread started")
        re_quit = re.compile('.*---- Closing idle connection')
        self.__fd = open(self.__path)
        line = None
        while line != '':# and not self.__lftp.ftp_engine.quit_event.isSet():
            line = self.__fd.readline()
            if re_quit.match(line):
                gtk.idle_add(self.__lftp.ftp_engine.emit,
                             'idle-connection-closed')
            idle_add(self.__log_updater,
                     unicode(line,
                             self.__lftp.ftp_engine.site_info.remote_charset))
        self.__fd.close()
        os.unlink(self.__path)
        logging.debug('fifo reading thread quit')

class LftpCommand:

    MSG_INTERRUPTED = 'Interrupted'

    def __init__(self, lftp, command):
        self.lftp = lftp
        self.command = command

    def execute(self):
        self.proc = self.lftp.proc
        if isinstance(self.command, unicode):
            self.proc.sendline(self.command.encode(
                self.lftp.ftp_engine.site_info.remote_charset))
        else:
            self.proc.sendline(self.command)
        logging.debug('send command %s' % self.proc.readline()[:-1])
        return self.parse_result()

    def parse_result(self):
        index = self.proc.expect(
            [LftpClient.PROMPT_STRING,
             self.MSG_INTERRUPTED
             ])
        if index == 0:
            result = string.rstrip(self.proc.before)
            logging.debug("return: " + result)
            return result
        elif index == 1:
            raise FTPInteruptedException()

class LftpVersionCommand(LftpCommand):

    def __init__(self, lftp):
        LftpCommand.__init__(self, lftp, "version")

class LftpOpenCommand(LftpCommand):

    UNKNOWN_HOST = 'open: (.*): Unknown host'

    def __init__(self, lftp, site_info):
        url = "ftp://%s:%d" % (site_info.server_name, site_info.server_port)
        username = site_info.username
        self.__password = site_info.password
        if username == 'None' and (self.__password == '' or self.__password == None):
            self.__password = 'unknown@coralstudio.org'
        remote_path = site_info.remote_path
        if remote_path == None or remote_path == '':
            remote_path = '-'
        if username != None:
            cmd = 'open -e "cd %s" -u %s %s' % (remote_path, username, url)
        else:
            cmd = 'open -e "cd %s" %s' % (remote_path, url)
        LftpCommand.__init__(self, lftp, cmd)

    def parse_result(self):
        index = self.proc.expect([
            LftpClient.PROMPT_STRING,
            LftpOpenCommand.UNKNOWN_HOST,
            "Password"])
        if index == 0:
            return
        elif index == 1:
            s = self.proc.match.string
            self.proc.expect(LftpClient.PROMPT_STRING)
            raise FTPUnknownHostException(s)
        elif index == 2:
            self.proc.sendline(self.__password)
            self.proc.readline()
            return self.parse_result()

class LftpCdCommand(LftpCommand):

    def __init__(self, lftp, path):
        if path == '':
            path = '/'
        LftpCommand.__init__(self, lftp, 'cd "%s"' % path)

    def parse_result(self):
        cpl = self.proc.compile_pattern_list([
            LftpClient.PROMPT_STRING,
            ".*: Access failed: (\d*) (.*)\r"])
        index = -1
        error = None
        while index != 0:
            index = self.proc.expect(cpl)
            if index == 1:
                m = self.proc.match
                error = FTPAccessFailedException(string.atoi(m.group(1)),
                                                 m.group(2))
        if error:
            raise error

class LftpListdCommand(LftpCommand):

    def __init__(self, lftp, apath):
        LftpCommand.__init__(self, lftp, 'ls -d -q "%s"' % apath)

class LftpListCommand(LftpCommand):

    def __init__(self, lftp, updater=None, dir='', clear_cache=0):
        self.__updater = updater
        if clear_cache:
            LftpCommand.__init__(self, lftp, 'rels %s' % dir)
        else:
            LftpCommand.__init__(self, lftp, 'ls %s' % dir)

    def parse_result(self):
        first = self.proc.compile_pattern_list([
            LftpClient.PROMPT_STRING, '([drwxsS\-]{10,11}) '])
        digit = self.proc.compile_pattern_list(['(\d+) '])
        word = self.proc.compile_pattern_list(['([\w\:]+) '])
        lastany = self.proc.compile_pattern_list([
            '(?: *)(.*)\r\n'])
        filelist = []
        while 1:
            index = self.proc.expect(first)
            if index == 0:
                break
            attr = self.proc.match.group(1)
            self.proc.expect(digit)
            childnum = self.proc.match.group(1)
            self.proc.expect(word)
            owner = self.proc.match.group(1)
            self.proc.expect(word)
            group = self.proc.match.group(1)
            self.proc.expect(digit)
            size = self.proc.match.group(1)
            self.proc.expect(word)
            month = self.proc.match.group(1)
            self.proc.expect(digit)
            day = self.proc.match.group(1)
            self.proc.expect(word)
            year_or_time = self.proc.match.group(1)
            self.proc.expect(lastany)
            name = unicode(self.proc.match.group(1),
                           self.lftp.ftp_engine.site_info.remote_charset)
            while ord(name[0]) == 27:
                pos = string.find(name, 'm')
                name = name[pos+1:]
            epos = string.rfind(name, chr(27))
            while epos >= 0:
                name = name[0:epos]
                epos = string.rfind(name, chr(27))
            filelist.append((attr, childnum,
                            owner, group, size, month, day,
                            year_or_time, name))
            if len(filelist) == 10:
                gtk.idle_add(self.__updater, filelist)
                filelist = []
        if len(filelist) > 0:
            gtk.idle_add(self.__updater, filelist)
        return
        
class LftpQuitCommand(LftpCommand):

    def __init__(self, lftp):
        LftpCommand.__init__(self, lftp, "quit")

    def parse_result(self):
        self.proc.expect([pexpect.EOF])
        return

class LftpPwdCommand(LftpCommand):

    def __init__(self, lftp, updater):
        LftpCommand.__init__(self, lftp, "pwd")
        self.__updater = updater

    def parse_result(self):
        r = LftpCommand.parse_result(self)
        gtk.idle_add(self.__updater, unicode(
            r, self.lftp.ftp_engine.site_info.remote_charset))
        return r

class LftpTransferCommand(LftpCommand):

    def __init__(self, lftp, command, running_updater, finish_updater):
        LftpCommand.__init__(self, lftp, command)
        self.__rupdater = running_updater
        self.__fupdater = finish_updater

    def parse_result(self):
        rec = re.compile(".*' at (\d+) .*\[(.+)\]")
        rec_prompt = re.compile(LftpClient.PROMPT_STRING)
        rec_failed = re.compile(".*: Access failed: (\d*) (.*)")
        line = ''
        error = None
        while 1:
            c = self.proc.read(1)
            if ord(c) == 13:
                #print 'line', len(line), line
                m = rec.match(line)
                if m != None:
                    gtk.idle_add(self.__rupdater, m.group(1), m.group(2))
                while ord(c) == 13:
                    c = self.proc.read(1)
                if ord(c) == 10:
                    break
                line = c
            else:
                line = line + c
                m = rec_prompt.match(line)
                if m != None:
                    # nothing transfered
                    gtk.idle_add(self.__fupdater)
                    return
                m = rec_failed.match(line)
                if m != None:
                    error = FTPAccessFailedException(string.atoi(m.group(1)),
                                                     m.group(2))
        if error:
            gtk.idle_add(self.__fupdater, 1)
            raise error
        if re.match('.*bytes transferred', line):
            gtk.idle_add(self.__fupdater)
        r = LftpCommand.parse_result(self)
        return

class LftpPutCommand(LftpTransferCommand):

    def __init__(self, lftp, source, target, running_updater, finish_updater):
        cmd = 'put "%s" -o "%s"' % (source, target)
        LftpTransferCommand.__init__(self, lftp, cmd,
                                     running_updater, finish_updater)
    
class LftpGetCommand(LftpTransferCommand):

    def __init__(self, lftp, source, target, resume, running_updater, finish_updater):
        if resume:
            cmd = 'get -c "%s" -o "%s"' % (source, target)
        else:
            cmd = 'get "%s" -o "%s"' % (source, target)
        LftpTransferCommand.__init__(self, lftp, cmd, running_updater, finish_updater)

class LftpDeleteCommand(LftpCommand):

    def __init__(self, lftp, name, isdir):
        if not isdir:
            LftpCommand.__init__(self, lftp, 'rm "%s"' % name)
        else:
            LftpCommand.__init__(self, lftp, 'rmdir "%s"' % name)

    def parse_result(self):
        cpl = self.proc.compile_pattern_list([
            LftpClient.PROMPT_STRING,
            ".* ok, (.*)\r",
            ".*: Access failed: (\d*) (.*)\r"])
        index = -1
        error = None
        while index != 0:
            index = self.proc.expect(cpl)
            if index == 1:
                # rm ok
                pass
            elif index == 2:
                m = self.proc.match
                error = FTPAccessFailedException(string.atoi(m.group(1)),
                                                 m.group(2))
        if error:
            raise error
        return

class LftpMoveCommand(LftpCommand):

    def __init__(self, lftp, old_name, new_name):
        LftpCommand.__init__(self, lftp, 'mv "%s" "%s"' % (old_name, new_name))

    def parse_result(self):
        cpl = self.proc.compile_pattern_list([
            LftpClient.PROMPT_STRING,
            "rename successful",
            ".*: Access failed: (\d*) (.*)\r"])
        index = -1
        error = None
        while index != 0:
            index = self.proc.expect(cpl)
            if index == 1:
                # rename ok
                pass
            elif index == 2:
                error = FTPAccessFailedException(string.atoi(m.group(1)),
                                                 m.group(2))
                pass
        if error:
            raise error
        return

class LftpMkdirCommand(LftpCommand):

    def __init__(self, lftp, name):
        LftpCommand.__init__(self, lftp, 'mk "%s"' % (name))

    def parse_result(self):
        cpl = self.proc.compile_pattern_list([
            LftpClient.PROMPT_STRING, "mkdir ok", "Access failed"])
        index = -1
        while index != 0:
            index = self.proc.expect(cpl)
            if index == 1:
                # rename ok
                pass
            elif index == 2:
                # rename fail
                pass
        return
